﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Linq;

namespace Rosetta.Composition
{
    public sealed class SimpleExecutor : SingleNodeVisitor
    {
        #region Fields

        private readonly Dictionary<Node, ResultEntry> results = new Dictionary<Node, ResultEntry>();
        private readonly Template template;

        public event Action<Node, object[]> ResultFound = delegate { };
        public event Action<NodeConnection> VisitingChild = delegate { };

        private static readonly Type inputterType = typeof(IInputter);
        private static readonly Type converterType = typeof(IConverter);
        private static readonly Type outputterType = typeof(IOutputter);

        #endregion

        #region Constructors

        public SimpleExecutor(Template template)
        {
            if (template == null)
            {
                throw new ArgumentNullException("template");
            }
            if (!TemplateValidator.IsComplete(template))
            {
                throw new ArgumentException("Template is not complete yet.", "template");
            }

            this.template = template;
        }

        #endregion

        #region Properties

        [CanBeNull]
        public TextWriter Log { get; set; }

        #endregion

        #region Public Methods

        public void Run()
        {
            template.Accept(this);
        }

        #endregion

        #region Protected Methods

        protected override void Visit(Node node)
        {
            ResultEntry resultEntry = GetResultEntry(node);

            // Check if we need to calculate the result for this Node
            if (!resultEntry.HasResult && node.CanHaveParents)
            {
                // Fill Arguments
                foreach (var parentConnection in node.Parents)
                {
                    var index = parentConnection.ArgumentIndex;
                    var parentEntry = GetResultEntry(parentConnection.BeginPoint);
                    if (!parentEntry.HasResult)
                    {
                        // No result yet, so start there and parent will call us again
                        parentConnection.BeginPoint.Accept(this);
                        return;
                    }

                    resultEntry.Arguments[index] = parentEntry.Result;
                    if (parentConnection.HasMultipleResults)
                    {
                        resultEntry.ArgumentsMulti[index] = true;
                    }
                }

                resultEntry.GetResult(this);

                ResultFound(node, resultEntry.Result);
            }

            // Check if we have to visit our child nodes
            if (node.CanHaveChildren)
            {
                foreach (var childConnection in node.Children)
                {
                    VisitingChild(childConnection);

                    childConnection.EndPoint.Accept(this);
                }
            }
        }

        #endregion

        #region Private Methods

        private ResultEntry GetResultEntry(Node node)
        {
            ResultEntry resultEntry;
            if (!results.TryGetValue(node, out resultEntry))
            {
                results[node] = resultEntry = new ResultEntry(node);
            }
            return resultEntry;
        }

        #endregion

        #region Class Methods

        public static void Run([NotNull] Template template)
        {
            new SimpleExecutor(template).Run();
        }

        #endregion

        #region Nested Types

        private sealed class ResultEntry
        {
            #region Constructors

            public ResultEntry(Node node)
            {
                AddIn = node.AddIn;
                MethodEntry = node.MethodEntry;

                if (MethodEntry.HasArgumentTypes)
                {
                    // Inputter/Outputter
                    var length = MethodEntry.ArgumentTypes.Length;
                    Arguments = new object[length][];
                    ArgumentsMulti = new bool[length];
                }
                else
                {
                    // Inputter
                    var inputter = (IInputter)AddIn;
                    inputter.UseContext(node.Context);

                    // Get result
                    var inputterMethod = inputterType.GetMethod("Get").MakeGenericMethod(MethodEntry.ReturnType);
                    Result = new[] { inputterMethod.Invoke(inputter, new[] { MethodEntry.Name }) };
                    HasResult = true;
                }

            }

            #endregion

            #region Properties

            public IAddIn AddIn { get; private set; }

            public object[][] Arguments { get; private set; }

            public bool[] ArgumentsMulti { get; private set; }

            public bool HasResult { get; private set; }

            public IMethodEntry MethodEntry { get; private set; }

            public object[] Result { get; set; }

            #endregion

            #region Public Methods

            public void GetResult(SimpleExecutor executor)
            {
                var argumentTypes = MethodEntry.ArgumentTypes;

                var outputter = AddIn as IOutputter;
                if (outputter == null)
                {
                    // Converter
                    argumentTypes = argumentTypes.Concat(new[] { MethodEntry.ReturnType }).ToArray();
                    var converterMethod = converterType.GetMethods().First(m => m.Name == "Convert" && m.GetGenericArguments().Length == argumentTypes.Length).MakeGenericMethod(argumentTypes);
                    Result = Run((Delegate)converterMethod.Invoke(AddIn, new[] { MethodEntry.Name }), Arguments, ArgumentsMulti);
                }
                else
                {
                    // Outputter
                    outputter.Log = executor.Log;
                    var outputterMethod = outputterType.GetMethods().First(m => m.Name == "Put" && m.GetGenericArguments().Length == argumentTypes.Length).MakeGenericMethod(argumentTypes);
                    Run((Delegate)outputterMethod.Invoke(AddIn, new[] { MethodEntry.Name }), Arguments, ArgumentsMulti);
                }

                HasResult = true;
            }

            #endregion

            #region Class Methods

            private static IEnumerable GetAllValues(object[] result)
            {
                foreach (IEnumerable arg in result)
                {
                    if (arg != null)
                    {
                        foreach (var obj in arg)
                        {
                            yield return obj;
                        }
                    }
                    else
                    {
                        yield return null;
                    }
                }
            }

            private static IEnumerable GetValues(object[][] args, bool[] argsMulti, int index)
            {
                var result = args[index];
                return argsMulti[index] ? GetAllValues(result) : result;
            }

            private static object[] Run(Delegate handler, object[][] args, bool[] argsMulti)
            {
                var result = new List<object>();

                switch (args.Length)
                {
                    case 1:
                        foreach (var arg1 in GetValues(args, argsMulti, 0))
                        {
                            result.Add(handler.DynamicInvoke(arg1));
                        }
                        break;
                    case 2:
                        foreach (var arg1 in GetValues(args, argsMulti, 0))
                        {
                            foreach (var arg2 in GetValues(args, argsMulti, 1))
                            {
                                result.Add(handler.DynamicInvoke(arg1, arg2));
                            }
                        }
                        break;
                    case 3:
                        foreach (var arg1 in GetValues(args, argsMulti, 0))
                        {
                            foreach (var arg2 in GetValues(args, argsMulti, 1))
                            {
                                foreach (var arg3 in GetValues(args, argsMulti, 2))
                                {
                                    result.Add(handler.DynamicInvoke(arg1, arg2, arg3));
                                }
                            }
                        }
                        break;
                    case 4:
                        foreach (var arg1 in GetValues(args, argsMulti, 0))
                        {
                            foreach (var arg2 in GetValues(args, argsMulti, 1))
                            {
                                foreach (var arg3 in GetValues(args, argsMulti, 2))
                                {
                                    foreach (var arg4 in GetValues(args, argsMulti, 3))
                                    {
                                        result.Add(handler.DynamicInvoke(arg1, arg2, arg3, arg4));
                                    }
                                }
                            }
                        }
                        break;
                }

                return result.ToArray();
            }

            #endregion
        }

        #endregion
    }
}